// [Twin] Copyright eBay Inc., Twin authors, and other contributors.
// This file is provided to you under the terms of the Apache License, Version 2.0.
// See LICENSE.txt and NOTICE.txt for license and copyright information.

package org.ebayopensource.twin;

import java.lang.reflect.*;
import java.util.*;

import java.awt.Point;
import java.awt.Dimension;
import java.awt.Rectangle;

import org.apache.commons.codec.binary.Base64;

import org.ebayopensource.twin.ScrollBar.Orientation;
import org.ebayopensource.twin.element.*;
import org.ebayopensource.twin.pattern.*;

/**
 * This class actually implements the behaviour of Element and its subinterfaces.
 * All Elements returned from Twin are generated by Element.create(). This returns proxy objects
 * that reflectively delegate to an instance of ElementImpl. Note that these are not themselves ElementImpl instances. 
 * The only reason ElementImpl implements Element is so we get a static check that all methods will be present at runtime.
 */
class ElementImpl extends RemoteResource implements Element {
	/** Win32 window class */
	private String className;
	/** UIAutomation AutomationId */
	private String id;
	/** Name property, cached from last fetch, for use in toString() */
	private String cachedName;
	
	/** 
	 * For internal use only. Creates an Element wrapping the given RemoteObject 
	 * This should be used instead of new Element(), as it will instantiate the correct subclass.
	 */
	public static Element create(RemoteObject o) {
		if(o == null)
			return null;
		
		List<Class<?>> interfaces = new ArrayList<Class<?>>();
		interfaces.add(Element.class);
		interfaces.add(RemoteResourceInterface.class);

		final Class<? extends ControlType> controlTypeInterface = NameMappings.getTypeInterface((String)o.properties.get("controlType"));
		if(controlTypeInterface.equals(Desktop.class))
			return new DesktopImpl(o.session);
		if(controlTypeInterface != null)
			interfaces.add(controlTypeInterface);
		
		List<Class<? extends ControlPattern>> controlPatternInterfaces = getControlPatternInterfaces(o);
		interfaces.addAll(controlPatternInterfaces);
		
		final ElementImpl impl = new ElementImpl(o, controlTypeInterface, controlPatternInterfaces);
		if(interfaces.isEmpty())
			return impl;
		
		final HashSet<Class<?>> implementedPatterns = new HashSet<Class<?>>();
		for(Class<?> iface : interfaces)
			if(isInterfaceExtending(iface,ControlPattern.class))
				implementedPatterns.add(iface);
		
		return (Element)Proxy.newProxyInstance(
				ElementImpl.class.getClassLoader(), 
				interfaces.toArray(new Class[interfaces.size()]),
				new InvocationHandler() {
					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
						Method implMethod = null;
						try {
							implMethod = impl.getClass().getMethod(method.getName(), method.getParameterTypes());
						} catch (NoSuchMethodException e) {
							implMethod = impl.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());							
						}
						Require requirement = implMethod.getAnnotation(Require.class);
						if(requirement != null) {
							for(Class<?> pattern : requirement.pattern())
								if(!implementedPatterns.contains(pattern))
									throw new TwinException("This "+(impl.getControlType() == null ? "Unknown" : impl.getControlType())+ 
											" does not implement the control pattern "+pattern.getSimpleName());
							if(requirement.type() != Void.class)
								if(controlTypeInterface != requirement.type())
									throw new TwinException("This "+(impl.getControlType() == null ? "Unknown" : impl.getControlType())+ 
											" is not of ControlType "+requirement.type().getSimpleName());									
						}
						try {
							return implMethod.invoke(impl, args);
						} catch (InvocationTargetException e) {
							throw e.getCause();
						}
					}
				}
			);
	}
	
	private static List<Class<? extends ControlPattern>> getControlPatternInterfaces(RemoteObject o) {
		List<Class<? extends ControlPattern>> result = new ArrayList<Class<? extends ControlPattern>>();
		if(o.properties.containsKey("controlPatterns")) {
			for(Object patternObj : (List<?>)o.properties.get("controlPatterns")) {
				String pattern = String.valueOf(patternObj);
				Class<? extends ControlPattern> matchingIface = NameMappings.getPatternInterface(pattern);
				if(matchingIface != null)
					result.add(matchingIface);
			}
		}
		return result;
	}

	/** Is c an interface that directly extends i? */
	static final boolean isInterfaceExtending(Class<?> c, Class<?> i) {
		if(!c.isInterface())
			return false;
		for(Class<?> d : c.getInterfaces())
			if(i.equals(d))
				return true;
		return false;
	}
	
	/** 
	 * For internal use only. Creates an Element wrapping the given RemoteObject 
	 * Element.create() should be used instead, as it will instantiate the correct subclass.
	 */	
	protected ElementImpl(
			RemoteObject o, 
			Class<? extends ControlType> controlType, 
			List<Class<? extends ControlPattern>> controlPatterns){
		super(o);
		this.controlType = controlType;
		this.controlPatterns = controlPatterns;
		this.className = (String)o.properties.get("className");
		this.id = (String)o.properties.get("id");
		this.cachedName = (String)o.properties.get("name");
	}
	/** 
	 * For internal use only. Creates an element without an associated RemoteObject. 
	 * Use with caution!
	 */
	protected ElementImpl(Application session, 
			Class<? extends ControlType> controlType, 
			List<Class<? extends ControlPattern>> controlPatterns){
		super(session);
		this.controlType = controlType;
		this.controlPatterns = controlPatterns;
	}
	public String getCachedName() {
		return cachedName;
	}
	public String getName() throws TwinException {
		return cachedName = (String)session.request("GET", getPath()+"/name", null);
	}
	public String getId() throws TwinException {
		return id;
	}
	public String getClassName() throws TwinException {
		return className;
	}
//	/** Get the ControlType of this element. This is an abstract description of the function performed by the element */
//	public ControlTypeEnum getControlType() {
//		return controlType;
//	}

	private Class<? extends ControlType> controlType;
	private List<Class<? extends ControlPattern>> controlPatterns;
	public Class<? extends ControlType> getControlType() {
		return controlType;
	}
	public List<Class<? extends ControlPattern>> getControlPatterns() {
		return controlPatterns;
	}
	public boolean is(Class<? extends Element> pattern) {
		if(pattern.isInterface())
			for(Class<?> iface : pattern.getInterfaces()) {
				if(iface == ControlPattern.class)
					return controlPatterns.contains(pattern);
				else if(iface == ControlType.class)
					return controlType == pattern;
			}
		// if it's not a control type or control pattern, return instanceof
		return pattern.isInstance(this);
	}
	public <T extends Element> T as(Class<T> pattern) {
		T t = pattern.cast(this); // generate exception if regular type doesn't match
		if(!is(pattern))
			throw new ClassCastException("Object does not support "+pattern.getSimpleName()+": "+this);		
		return t;
	}
	
	@Require(pattern=Editable.class)
	public String getValue() throws TwinException {
		return (String)session.request("GET", getPath()+"/value", null);
	}
	@Require(pattern=Editable.class)
	public void setValue(String s) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("value", s);
		session.request("POST", getPath()+"/value", data);
	}
	@Require(pattern=Editable.class)
	public boolean isReadOnly() throws TwinException {
		return session.options(getPath()+"/value").contains("POST");
	}
	
	public boolean isEnabled() throws TwinException {
		return (boolean)(Boolean)session.request("GET", getPath()+"/enabled", null);
	}
	
	@Require(pattern=Expandable.class)
	public boolean isExpanded() throws TwinException {
		return (boolean)(Boolean)session.request("GET", getPath()+"/expanded", null);
	}
	@Require(pattern=Expandable.class)
	public void setExpanded(boolean expanded) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("expanded", expanded);
		session.request("POST", getPath()+"/expanded", data);
	}

	@Require(pattern=Selectable.class)
	public boolean isSelected() throws TwinException {
		return (boolean)(Boolean)session.request("GET", getPath()+"/selected", null);
	}
	@Require(pattern=Selectable.class)
	public void setSelected(boolean selected) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("selected", selected);
		session.request("POST", getPath()+"/selected", data);
	}
	@Require(pattern=Selectable.class)
	public SelectionContainer getContainer() throws TwinException {
		RemoteObject container = (RemoteObject)session.request("GET", getPath()+"/selection-container", null);
		return (SelectionContainer)ElementImpl.create(container);
	}
	
	@Require(pattern=SelectionContainer.class)
	public boolean isMultipleSelectionAllowed() throws TwinException {
		Map<String,Object> result = session.requestObject("GET", getPath()+"/selection", null);
		return (boolean)(Boolean)result.get("multiple");
	}
	@Require(pattern=SelectionContainer.class)
	public boolean isSelectionRequired() throws TwinException {
		Map<String,Object> result = session.requestObject("GET", getPath()+"/selection", null);
		return (boolean)(Boolean)result.get("required");
	}
	@Require(pattern=SelectionContainer.class)
	public List<Selectable> getSelection() throws TwinException {
		Map<String,Object> result = session.requestObject("GET", getPath()+"/selection-info", null);
		List<?> selectionObjects = (List<?>)result.get("values");
		List<Selectable> ret = new ArrayList<Selectable>();
		for(Object obj : selectionObjects) {
			if(!(obj instanceof Selectable))
				throw new IllegalStateException("getSelection() /selection-info contained "+obj+" which is not selectable");
			ret.add((Selectable)obj);
		}
		return ret;
	}
	@Require(pattern=SelectionContainer.class)
	public Selectable getSelectedItem() throws TwinException {
		List<Selectable> selection = getSelection();
		if(selection.size() == 0)
			return null;
		if(selection.size() == 1)
			return selection.get(0);
		throw new TwinException("Expected 0 or 1 result, got "+selection);
	}
	
	public Dimension getSize() throws TwinException {
		Map<String,Object> results = session.requestObject("GET", getPath()+"/bounds", null);
		int width = ((Number)results.get("width")).intValue();
		int height = ((Number)results.get("height")).intValue();
		return new Dimension(width, height);
	}
	public Point getLocation() throws TwinException {
		Map<String,Object> results = session.requestObject("GET", getPath()+"/bounds", null);
		int x = ((Number)results.get("x")).intValue();
		int y = ((Number)results.get("y")).intValue();
		return new Point(x, y);
	}
	public Rectangle getBounds() throws TwinException {
		Map<String,Object> results = session.requestObject("GET", getPath()+"/bounds", null);
		int width = ((Number)results.get("width")).intValue();
		int height = ((Number)results.get("height")).intValue();
		int x = ((Number)results.get("x")).intValue();
		int y = ((Number)results.get("y")).intValue();
		return new Rectangle(x, y, width, height);
	}
	
	@Require(pattern=Transformable.class)
	public void setSize(int width, int height) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("width",width);
		data.put("height",height);
		session.request("POST", getPath()+"/size", data);
	}
	@Require(pattern=Transformable.class)
	public void setLocation(int x, int y) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("x",x);
		data.put("y",y);
		session.request("POST", getPath()+"/location", data);
	}
	@Require(pattern=Transformable.class)
	public void setBounds(int x, int y, int width, int height) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("x",x);
		data.put("y",y);
		data.put("width",width);
		data.put("height",height);
		session.request("POST", getPath()+"/bounds", data);
	}
	
	@Require(pattern=Toggle.class)
	public boolean getState() throws TwinException {
		return (boolean)(Boolean)session.request("GET", getPath()+"/toggle", null);
	}
	@Require(pattern=Toggle.class)
	public void setState(boolean b) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("state", b);
		session.request("POST", getPath()+"/toggle", data);
	}
	@Require(pattern=Toggle.class)
	public boolean toggle() throws TwinException {
		return (boolean)(Boolean)session.request("POST", getPath()+"/toggle", null);
	}
	
	public void click() throws TwinException {
		session.request("POST", getPath()+"/click", null);
	}
	public void click(MouseButton button) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("button", button.toString().toLowerCase());
		session.request("POST", getPath()+"/click", data);
	}
	public void click(int x, int y) throws TwinException {
		click(x, y, MouseButton.Left);
	}
	public void click(int x, int y, MouseButton button) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("button", button.toString().toLowerCase());
		data.put("x", x);
		data.put("y", y);
		session.request("POST", getPath()+"/click", data);
	}
	public Menu contextMenu() throws TwinException {
		click(MouseButton.Right);
		return (Menu)session.getDesktop().waitForDescendant(Criteria.type(Menu.class), 1);
	}
	public Menu contextMenu(int x, int y) throws TwinException {
		click(x, y, MouseButton.Right);
		return (Menu)session.getDesktop().waitForDescendant(Criteria.type(Menu.class), 1);
	}
	
	public String getStructure(boolean verbose) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("verbose", verbose);
		return (String)session.request("GET", getPath()+"/structure", data);
	}
	public String getStructure() throws TwinException {
		return getStructure(false);
	}
	
	public Screenshot getScreenshot() throws TwinException {
		return decodeScreenshot(session.requestObject("GET", getPath()+"/screenshot", null));
	}
	public Screenshot getScreenshot(Rectangle bounds) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("x", bounds.x);
		data.put("y", bounds.y);
		data.put("width", bounds.width);
		data.put("height", bounds.height);
		return decodeScreenshot(session.requestObject("GET", getPath()+"/screenshot", data));
	}
	public Screenshot getBoundsScreenshot() throws TwinException {
		return getApplication().getDesktop().getScreenshot(getBounds());
	}
	
	public void sendKeys(String text) throws TwinException {
		Map<String,Object> keys = new HashMap<String,Object>();
		keys.put("keys", text);
		session.request("POST", getPath()+"/keyboard", keys);		
	}
	public void type(String text) throws TwinException {
		text = text.replaceAll("[\\+\\^\\%\\(\\)\\{\\}\\[\\]]", "{$0}");
		text = text.replace("\n", "~");
		text = text.replace("\b", "{BS}");
		text = text.replace("\t", "{TAB}");
		sendKeys(text);
	}
	private Element cachedParent;
	public Element getCachedParent() {
		if(cachedParent == null)
			return getParent();
		return cachedParent;
	}
	public Element getParent() throws TwinException { 
		return cachedParent = ElementImpl.create((RemoteObject)session.request("GET", getPath()+"/parent", null));
	}
	public List<Element> getChildren() throws TwinException {
		return getChildren(null);
	}
	public <T extends Element> List<T>  getChildren(Criteria criteria) throws TwinException {
		return getElements("children", criteria, 0, 0, false);
	}
	public List<Element> getDescendants(Criteria criteria) throws TwinException {
		return getElements("descendants", criteria, 0, 0, false);
	}
	public <T extends Element> T getChild(Criteria criteria) throws TwinException {
		return single(this.<T>getElements("children", criteria, 0, 0, true));
	}
	public <T extends Element> T waitForChild(Criteria criteria) throws TwinException {
		return this.<T>waitForChild(criteria, getApplication().getTimeout());
	}
	public <T extends Element> T waitForChild(Criteria criteria, double timeout) throws TwinException {
		return single(this.<T>getElements("children", criteria, 0, timeout, true));		
	}
	public <T extends Element> T getDescendant(Criteria criteria) throws TwinException {
		return single(this.<T>getElements("descendants", criteria, 0, 0, true));
	}
	public <T extends Element> T waitForDescendant(Criteria criteria) throws TwinException {
		return this.<T>waitForDescendant(criteria, getApplication().getTimeout());	
	}
	public <T extends Element> T waitForDescendant(Criteria criteria, double timeout) throws TwinException {
		return single(this.<T>getElements("descendants", criteria, 0, timeout, true));
	}
	public <T extends Element> List<T> getClosestDescendants(Criteria criteria) throws TwinException {
		return getElements("descendants", criteria, 1, 0, false);
	}
	public <T extends Element> List<T> waitForClosestDescendants(Criteria criteria) throws TwinException {
		return waitForClosestDescendants(criteria, getApplication().getTimeout());
	}
	public <T extends Element> List<T> waitForClosestDescendants(Criteria criteria, double timeout) throws TwinException {
		return getElements("descendants", criteria, 1, timeout, false);
	}
	/** 
	 * Internal impl behind {get,waitFor}{Closest,}{Child,Children,Descendant,Descendants} methods 
	 * @param path what to append to getPath(), e.g. "/children" or "/descendants"
	 * @param criteria the criteria to apply (process-ID matching is added by the server)
	 * @param count if 0, return all results. Else BFS layer-by-layer until we have at least count
	 * @param timeout if 0, return results immediately. Else don't return an empty result set until this timeout has elapsed
	 * @param shouldThrow should throw a TwinNoSuchElementException on empty list
	 */
	@SuppressWarnings("unchecked")
	private <T extends Element> List<T> getElements(String subpath, Criteria criteria, int count, double timeout, boolean shouldThrow) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		if(criteria != null)
			data.put("criteria", criteria);
		if(count > 0)
			data.put("count", count);
		if(timeout > 0) {
			if(Double.isInfinite(timeout))
				data.put("waitForResults", true);
			else
				data.put("waitForResults", timeout);
		}
		List<Object> searchResults = session.requestArray("GET", getPath()+"/"+subpath, data);
		List<T> result = new ArrayList<T>();
		for(Object remote : searchResults)
			result.add((T)ElementImpl.create((RemoteObject)remote));
		if(shouldThrow && result.isEmpty()) {
			String message = "Found no "+subpath+" of "+this;
			if(criteria != null)
				message += " matching "+criteria;
			if(timeout > 0)
				message += " after waiting "+timeout;
			throw TwinError.NoSuchElement.create(message);
		}
		return result;
	}
	/** Return the single element in a single list, null for an empty list, and throw for a list with multiple entries */
	private <T> T single(List<T> list) throws TwinException {
		if(list.size() == 1)
			return list.get(0);
		throw new TwinException("Expected 1 result, found "+list.size());
	}
	public void focus() throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("focusedElement", remote);
		session.request("POST", "/element/active", data);
	}
	/** Decode a screenshot from the result object */
	private Screenshot decodeScreenshot(Map<String,Object> results) {
		String contentType = (String) results.get("contentType");
		String base64data = (String) results.get("data");
		byte[] data = new Base64().decode(base64data);
		return new Screenshot(data, contentType);
	}
	/** Get the server path for this element */
	public String getPath() {
		return "/element/"+remote.uuid;
	}
	/** Return a string including the controltype, last knownname, className, id, and server UUID of this element */
	public String toString() {
		StringBuffer sb = new StringBuffer(NameMappings.getTypeName(controlType));
		sb.append("(");
		if(cachedName != null)
			sb.append("name=").append(cachedName).append(' ');
		if(className != null)
			sb.append("class=").append(className).append(' ');
		sb.append("id=").append(id).append(")");
		sb.append("@").append(remote);
		return sb.toString();
	}
	public ScrollBar getHorizontalScrollBar() throws TwinException {
		return getScrollBar(ScrollBar.Orientation.Horizontal);
	}
	public ScrollBar getVerticalScrollBar() throws TwinException {
		return getScrollBar(ScrollBar.Orientation.Vertical);
	}
	public ScrollBar getScrollBar(ScrollBar.Orientation orientation) throws TwinException {
		String resource = (orientation == ScrollBar.Orientation.Horizontal) ? "axisX" : "axisY";
		RemoteObject o = (RemoteObject)session.request("GET", getPath()+"/"+resource, null);
		if(o == null)
			throw TwinError.NoSuchElement.create("Couldn't get "+orientation+" scrollbar for element "+this);
		return new ScrollBarImpl(o, this, orientation);
	}
	public Element button(String name) throws TwinException {
		return single(this.getClosestDescendants(Criteria.type(Button.class).and(Criteria.name(name))));
	}
	
	public boolean exists() throws TwinException {
		return (Boolean)session.request("GET", getPath()+"/exists", null);
	}
	public void waitForNotExists() throws TwinException {
		waitForNotExists(getApplication().getTimeout());
	}
	public void waitForNotExists(double timeout) throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("timeout", timeout);
		data.put("value", false);

		session.request("POST", getPath()+"/exists", data);
	}
	public void scrollVisible(Element child) throws TwinException {
		Rectangle bounds = getBounds();
		Rectangle childBounds = child.getBounds();
		scrollVisible(child, Orientation.Horizontal, bounds, childBounds);
		scrollVisible(child, Orientation.Vertical, bounds, childBounds);
	}
	public void scrollVisible(Element child, Orientation orientation) {
		scrollVisible(child, orientation, getBounds(), child.getBounds());
	}
	private void scrollVisible(Element child, Orientation orientation, Rectangle bounds, Rectangle childBounds) {
		// if we're in bounds, we're done and don't need to check for a bar
		if(orientation.contains(bounds, childBounds))
			return;
		
		// sanity check: child may be too big
		if(orientation.getSize(childBounds) > orientation.getSize(bounds))
			throw new TwinException("Cannot scroll element "+child+" visible inside "+this+" for orientation "+orientation+": child is bigger than parent");
		
		ScrollBar bar = getScrollBar(orientation);
		if(bar == null)
			throw new TwinException("Cannot scroll element "+child+" visible inside "+this+" for orientation "+orientation+": no scrollbar");
		
		// test positions at 1.0 and 0.0, if neither match then we can use the results to interpolate
		bar.setPosition(1);
		Rectangle maxBounds = child.getBounds();
		if(orientation.contains(bounds, maxBounds)) // cheap client side check, maybe we're done
			return;
		
		bar.setPosition(0);
		Rectangle minBounds = child.getBounds();
		if(orientation.contains(bounds, minBounds)) // cheap client side check, maybe we're done
			return;
		
		// the pixel difference between max-scroll and min-scroll
		double scrollHeight = orientation.getMid(maxBounds) - orientation.getMid(minBounds);
		if(scrollHeight == 0) // scroll bar does nothing
			throw new TwinException("Cannot scroll element "+child+" visible inside "+this+" for orientation "+orientation+": it does not move in response to scrollbar action");

		// the difference between centre-of-child-at-minscroll and centre-of-this
		double offsetFromMinInPixels = orientation.getMid(bounds) - orientation.getMid(minBounds);
		// same thing, but scaled to scroll height
		double offsetFromMinAsFraction = offsetFromMinInPixels / scrollHeight;
		
		// clamp to [0,1] range
		if(offsetFromMinAsFraction < 0)
			offsetFromMinAsFraction = 0;
		if(offsetFromMinAsFraction > 1)
			offsetFromMinAsFraction = 1;
		
		bar.setPosition(offsetFromMinAsFraction);
		// now we should be done... maybe we should do some sanity check here
	}
	
	@Require(type=Menu.class)
	public MenuItem item(int index) throws TwinException {
		List<Element> elements = getDescendants(Criteria.type(MenuItem.class));
		return (MenuItem)elements.get(index);
	}
	@Require(type=Menu.class)
	public MenuItem item(String name) throws TwinException {
		return (MenuItem)getDescendant(Criteria.type(MenuItem.class).and(Criteria.name(name)));
	}

	@Require(type=MenuItem.class)
	public Menu openMenu() throws TwinException {
		click();
		try {
			return (Menu)waitForChild(Criteria.type(Menu.class), 1);
		} catch (TwinNoSuchElementException e) {
			Menu open = session.getOpenMenu();
			if(open == null)
				throw TwinError.NoSuchElement.create("Couldn't find child menu");
			return open;
		}
	}
	
	@Require(type=Window.class)
	public MenuItem menu(String name) throws TwinException {
		return (MenuItem)getDescendant(Criteria.type(MenuItem.class).and(Criteria.name(name)));
	}
	@Require(type=Window.class)
	public void close() throws TwinException {
		remote.session.request("DELETE", getPath(), null);
	}
	@Require(type=Window.class)
	public boolean isMaximized() throws TwinException {
		return "maximized".equalsIgnoreCase((String)remote.session.request("GET", getPath()+"/window-state", null));
	}
	@Require(type=Window.class)
	public boolean isMinimized() throws TwinException {
		return "minimized".equalsIgnoreCase((String)remote.session.request("GET", getPath()+"/window-state", null));		
	}
	@Require(type=Window.class)
	public void maximize() throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("state", "maximized");
		remote.session.request("POST", getPath()+"/window-state", data);
	}
	@Require(type=Window.class)
	public void minimize() throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("state", "minimized");
		remote.session.request("POST", getPath()+"/window-state", data);
	}
	@Require(type=Window.class)
	public void restore() throws TwinException {
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("state", "normal");
		remote.session.request("POST", getPath()+"/window-state", data);		
	}
}
